package com.thornbird.vehiclerecycling.model.record;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.thornbird.vehiclerecycling.entity.BaseEntity;
import com.thornbird.vehiclerecycling.model.record.property.RecordProperties;
import com.thornbird.vehiclerecycling.repository.UserRepository;
import com.thornbird.vehiclerecycling.util.DateUtil;
import com.thornbird.vehiclerecycling.util.JsonUtil;
import org.springframework.beans.BeanUtils;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;

public abstract class Record<T> {

    private Long id;

    @JsonFormat(pattern = DateUtil.DATETIME_FORMAT)
    private Date createdAt;

    @JsonFormat(pattern = DateUtil.DATETIME_FORMAT)
    private Date updatedAt;

    public static <R extends Record, E> List<R> convertList(List<E> list, Class<R> clazz) {
        return convertList(list, clazz, null);
    }

    public static <R extends Record, E> List<R> convertList(List<E> list, Class<R> clazz, RecordProperties properties) {
        return convertList(list, clazz, properties, null);
    }

    public static <R extends Record, E> List<R> convertList(List<E> list, Class<R> clazz, RecordProperties properties,
                                                            UserRepository userRepository) {
        List<R> recordList = null;
        if (list == null) {
            return recordList;
        }

        recordList = new ArrayList<>();
        for (E entity : list) {
            R record = null;
            try {
                record = clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            if (record != null) {
                record.parseEntity(entity, properties, userRepository);
                recordList.add(record);
            }
        }
        return recordList;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public Date getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(Date updatedAt) {
        this.updatedAt = updatedAt;
    }

    public void parseEntity(T entity) {
        parseEntity(entity, null);
    }

    public void parseEntity(T entity, RecordProperties properties) {
        if (entity == null) {
            return;
        }

        String[] ignoreProperties = null;
        if (properties != null && properties.getIgnoreProperties() != null) {
            ignoreProperties = properties.getIgnoreProperties().toArray(new String[0]);
        }
        BeanUtils.copyProperties(entity, this, ignoreProperties);

        if (properties != null && properties.getEntityProperties() != null) {
            Set<String> keySet = properties.getEntityProperties().keySet();
            for (String key : keySet) {
                try {
                    copyEntityProperty(key, entity, properties.getEntityProperties().get(key));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void parseEntity(T entity, RecordProperties properties, UserRepository userRepository) {
        parseEntity(entity, properties);
    }

    public void copyToEntity(T entity, String... ignoreProperties) {
        if (entity != null) {
            BeanUtils.copyProperties(this, entity, ignoreProperties);
        }
    }

    @Override
    public String toString() {
        return JsonUtil.toJsonString(this);
    }

    @SuppressWarnings("unchecked")
    private void copyEntityProperty(String property, T entity, RecordProperties properties) throws Exception {
        if (entity == null) {
            return;
        }

        Method recordGetter = getGetterMethod(this.getClass(), property);
        Method recordSetter = getSetterMethod(this.getClass(), property);
        Class<?> recordClass = recordGetter.getReturnType();
        Method entityGetter = getGetterMethod(entity.getClass(), property);
        if (recordClass.isAssignableFrom(List.class)) {
            List sourceList = (List) entityGetter.invoke(entity);
            if (sourceList != null) {
                List<Object> targetList = new ArrayList<>();
                Type type = ((ParameterizedType) recordGetter.getGenericReturnType()).getActualTypeArguments()[0];
                Class<?> itemClass = (Class<?>) type;
                for (Object item : sourceList) {
                    Object targetItem = itemClass.newInstance();
                    ((Record) targetItem).parseEntity(item, properties);
                    targetList.add(targetItem);
                }
                recordSetter.invoke(this, targetList);
            }
        } else {
            BaseEntity sourceValue = (BaseEntity) entityGetter.invoke(entity);
            if (sourceValue != null) {
                Object targetValue = recordClass.newInstance();
                ((Record) targetValue).parseEntity(sourceValue, properties);
                recordSetter.invoke(this, targetValue);
            }
        }
    }

    private Method getGetterMethod(Class clazz, String property) {
        Method getter = null;
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.getName().startsWith("get") &&
                    method.getName().substring(3).toLowerCase().equals(property.toLowerCase())) {
                getter = method;
                break;
            }
        }
        return getter;
    }

    private Method getSetterMethod(Class clazz, String property) {
        Method setter = null;
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.getName().startsWith("set") &&
                    method.getName().substring(3).toLowerCase().equals(property.toLowerCase())) {
                setter = method;
                break;
            }
        }
        return setter;
    }

}
